﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Asol.Reporting.Support;
using Asol.Reporting.Support.Parser;

namespace Asol.Reporting.Editor.SqlCommands
{
    #region class SqlCommand : třída, která zpracuje text SQL commandu na řadu objektů, které obsahují zadané příkazy.
    /// <summary>
    /// SqlCommand : třída, která zpracuje text SQL commandu na řadu objektů, které obsahují zadané příkazy.
    /// </summary>
    public class SqlCommand
    {
        /// <summary>
        /// Zpracuje daný SQL text na jednotlivé příkazy, a ty vrátí,
        /// </summary>
        /// <param name="sqlText"></param>
        /// <returns></returns>
        public static IEnumerable<ISqlCommand> Parse(string sqlText)
        {
            ParserStream sqlStream = ParserStream.ParseSqlCommand(sqlText);
            return Parse(sqlStream);
        }
        /// <summary>
        /// Zpracuje daný SQL stream na jednotlivé příkazy, a ty vrátí.
        /// </summary>
        /// <param name="sqlStream"></param>
        /// <returns></returns>
        public static IEnumerable<ISqlCommand> Parse(ParserStream sqlStream)
        {
            List<ISqlCommand> result = new List<ISqlCommand>();
            List<ISqlCommand> allCommands = new List<ISqlCommand>(Assistant.GetServiceObjectList<ISqlCommand>(true));

            while (!sqlStream.EndOfStream)
            {
                int pointer = sqlStream.Pointer;

                ISqlCommand command = FindSqlCommand(sqlStream, allCommands);
                if (command != null)
                {
                    ISqlCommand sqlCommand = Assistant.GetServiceObject<ISqlCommand>(command.GetType(), false);
                    sqlCommand.LoadFrom(sqlStream);
                    if (sqlStream.Pointer == pointer)
                        throw new DevException("SqlCommand parser error: command «%0» did not process a word.", command.GetType().Name);
                    result.Add(sqlCommand);
                }
                else
                    sqlStream.ReadValue();
            }

            return result;
        }
        /// <summary>
        /// Najde v seznamu všech existujících SQL příkazů ten, který bude zpracovávat aktulání příkaz ze SqlStreamu.
        /// Pokud se nikdo nepřihlásí, vrátí null.
        /// </summary>
        /// <param name="sqlStream"></param>
        /// <param name="allCommands"></param>
        /// <returns></returns>
        private static ISqlCommand FindSqlCommand(ParserStream sqlStream, List<ISqlCommand> allCommands)
        {
            foreach (ISqlCommand commands in allCommands)
            {
                if (commands.IsMyPart(sqlStream))
                    return commands;
            }
            return null;
        }
        /// <summary>
        /// Konstanta pro typ segmentu "CASE"
        /// </summary>
        public const string EXPR_CASE = "CASE";
    }
    #endregion
    #region class SqlPart : abstraktní třída, která je vhodným předkem pro všechny SQL parts.
    /// <summary>
    /// SqlPart : abstraktní třída, která je vhodným předkem pro všechny SQL parts.
    /// Umožní potomkovi deklarovat jeho sady klíčových slov (kombinace slov), jimiž může / musí začínat (property this.KeyWords).
    /// Zajistí za něj detekci ISqlPart.IsMyPart() = prohledá seznam klíčových slov, a podle výsledku odpoví.
    /// </summary>
    public abstract class SqlPart : ISqlPart
    {
        #region Servisní metody
        #endregion
        #region Public property a metody
        /// <summary>
        /// Vizualizace
        /// </summary>
        /// <returns></returns>
        public override string ToString() { return this.Text; }
        /// <summary>
        /// Obsahuje textovou reprezentaci této SqlPart.
        /// Pokud je IsUsed = false, obsahuje prázdný string.
        /// Pokud je IsUsed = true, pak ve výchozím stavu obsahuje souhrn Values.
        /// Do této property lze vložit explicitní hodnotu, pak vrací ji.
        /// Pokud bude vloženo null, bude zase vracet výchozí hodnotu.
        /// </summary>
        public virtual string Text
        {
            get
            {
                if (!this.IsUsed) return "";
                if (this.TextExplicit != null) return this.TextExplicit;
                return ParserStream.ToText(this.Values);
            }
            set
            {
                this.TextExplicit = value;
                this.IsUsed = (value != null || this.StreamLength > 0);
            }
        }
        /// <summary>
        /// Explicitně vložený text
        /// </summary>
        protected string TextExplicit;
        /// <summary>
        /// Příznak, že tato část SQL příkazu byla nalezena a použita.
        /// Každá část SQL příkazu je načítána ze SQL streamu, ale ne každá část je v něm skutečně přítomna.
        /// </summary>
        public bool IsUsed { get; protected set; }
        /// <summary>
        /// Text reálně nalezeného klíčového slova.
        /// </summary>
        public string Keyword { get; protected set; }
        /// <summary>
        /// Text chyby nalezené v této části
        /// </summary>
        public string Error { get; protected set; }
        /// <summary>
        /// Položka segmentu, která nebyla očekávaná a na níž došlo k chybě (this.Error)
        /// </summary>
        public ParserSegmentValue ErrorAtValue { get; protected set; }
        /// <summary>
        /// Ukazatel na první položku streamu, která je načtena do této SqlPart.
        /// </summary>
        public int StreamPointer { get; protected set; }
        /// <summary>
        /// Počet prvků streamu načtených do této SqlPart.
        /// </summary>
        public int StreamLength { get; protected set; }
        /// <summary>
        /// Prvky streamu, které byly načteny do této SqlPart.
        /// </summary>
        public IEnumerable<ParserSegmentValue> Values { get { return (this.Stream == null ? null : this.Stream.SubPart(this.StreamPointer, this.StreamLength)); } }
        /// <summary>
        /// Stream, který se do této SqlPart načítal
        /// </summary>
        public ParserStream Stream { get; protected set; }
        #endregion
        #region Protected virtual property a metody
        /// <summary>
        /// Property vrací sadu klíčových slov, které mohou uvozovat tuto část.
        /// Může být více formulací, odděleny musí být čárkou nebo středníkem. (například "LEFT OUTER JOIN, RIGHT JOIN", atd).
        /// Jednotlivý prvek je jedna kombinace ("LEFT OUTER JOIN"). Jednotlivá slova jsou oddělena mezerou.
        /// Prvek může mít více kombinací klíčových slov.
        /// Je třeba na první pozice dávat delší texty, a na pozdější pozice slova kratší, protože se vyhledává první odpovídající slovo.
        /// Na místě klíčového slova může být OTAZNÍK, což značí "na tomto místě cokoliv". Například "LEFT OUTER ? JOIN" povoluje použití hintů (konkrétně: "LEFT OUTER HASH JOIN").
        /// </summary>
        protected virtual string Keywords { get { return null; } }
        /// <summary>
        /// Metoda je volána tehdy, když bylo zjištěno, že aktuální stream odpovídá některému z klíčových slov tohoto objektu.
        /// Nalezené klíčové slovo je předáno v parametru.
        /// V této chvíli, když je volána tato metoda, je již nastavena property this.IsUsed na true.
        /// Tato metoda v bázové třídě ze streamu odebere klíčové slovo (všechny jeho prvky), takže ve streamu jsou na řadě následující hodnoty.
        /// Toto přetížení (se dvěma parametry) nikam neukládá reálně přečtená slova.
        /// Existuje přetížení s out parametrem values, které ukládá reálně nalezené položky do výstupu, aby si je volající mohl zpracovat (typicky vyhledá slova odpovídající wildcards "?").
        /// </summary>
        /// <param name="stream">ParserStream</param>
        /// <param name="keyword">Klíčové slovo, které bylo reálně nalezeno</param>
        protected virtual void Load(ParserStream stream, string keyword)
        {
            stream.SkipKeyword(keyword);
        }
        /// <summary>
        /// Metoda je volána tehdy, když bylo zjištěno, že aktuální stream odpovídá některému z klíčových slov tohoto objektu.
        /// Nalezené klíčové slovo je předáno v parametru.
        /// V této chvíli, když je volána tato metoda, je již nastavena property this.IsUsed na true.
        /// Tato metoda v bázové třídě ze streamu odebere klíčové slovo (všechny jeho prvky), takže ve streamu jsou na řadě následující hodnoty.
        /// Toto přetížení s out parametrem values ukládá reálně nalezené položky do výstupu, aby si je volající mohl zpracovat (typicky vyhledá slova odpovídající wildcards "?").
        /// </summary>
        /// <param name="stream">ParserStream</param>
        /// <param name="keyword">Klíčové slovo, které bylo reálně nalezeno</param>
        protected virtual void Load(ParserStream stream, string keyword, out List<ParserSegmentValue> values)
        {
            stream.SkipKeyword(keyword, out values);
        }
        #endregion
        #region Detekce klíčových slov IsMySqlPart()
        /// <summary>
        /// Metoda má zjistit, zda stream aktuálně ukazuje na slovo,
        /// kterým začíná zdejší část SQL příkazu.
        /// Bázová metoda ve třídě SqlPart získá seznam klíčových slov z property this.KeyWords,
        /// a vrátí true pokud aktuální stream začíná některým z nich.
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        protected virtual bool IsMySqlPart(ParserStream stream)
        {
            return stream.StartsWithKeyword(this.Keywords);
        }
        #endregion
        #region Implementace ISqlPart
        /// <summary>
        /// Metoda zjistí, zda ve streamu je jako první (platné) slovo to slovo, které je klíčem k této třídě.
        /// Například klíčové slovo pro příkaz SELECT je slovo "SELECT", pro frázi FROM je to slovo "FROM",
        /// pro frázi JOIN to může být "LEFT", "RIGHT", "OUTER", "INNER", "JOIN", 
        /// pro frázi ORDER BY musí být po sobě dvě slova "ORDER" a "BY", a další podobná pravidla.
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public bool IsMyPart(ParserStream stream)
        {
            using (stream.Preview())
            {   // Nakoukneme do streamu, jaké je první významné slovo v příkazu:
                return this.IsMySqlPart(stream);
            }
        }
        /// <summary>
        /// Metoda zajistí vyvolání načtení dat ze streamu přímo = bez testů Keywords.
        /// Označí objekt jako IsUsed = true a vyvolá protected metodu Load s parametrem keyWord = null.
        /// </summary>
        /// <param name="stream"></param>
        public void LoadDirect(ParserStream stream)
        {
            this.IsUsed = true;
            this.StreamInit(stream);
            this.Load(stream, null);
            this.StreamDone(stream);
        }
        /// <summary>
        /// Metoda může být volaná kdykoliv, za jakéhokoli stavu streamu - tedy i když stream neobsahuje data pro tuto parts.
        /// Objekt pak nesmí stream posunout, ale musí v klidu skončit (ani chybu nemá vyhodit).
        /// Tato technika je potřebná pro sekvenční načítání vnitřních částí SQL příkazu, kdy SqlCommand obsahuje sekvenci příkazů Part.LoadForm() pro jednotlivé své SqlParts.
        /// Pořadí částí v SQL příkazu je povinné, proto je možno v pevně daném pořadí načítat jednotlivé Parts.
        /// Některé Parts jsou nepovinné, takže mají zůstat prázdné.
        /// Tato metoda v takovém případě uloží do objektu příznak "Not used part".
        /// Objekt do sebe načte data přicházející ze streamu, stream se tím posouvá.
        /// Jakmile objekt narazí na část streamu, kterou nezná, skončí.
        /// </summary>
        /// <param name="stream"></param>
        public void LoadFrom(ParserStream stream)
        {
            this.StreamInit(stream);

            this.Keyword = null;
            using (stream.Preview())
            {   // Nakoukneme do streamu, zda obsahuje některé naše klíčové slovo, a případně které to je:
                this.Keyword = stream.FindInitialKeyword(this.Keywords);
            }

            this.IsUsed = (this.Keyword != null);

            if (this.IsUsed)
            {
                this.Keyword = this.Keyword;
                this.Load(stream, this.Keyword);
            }

            this.StreamDone(stream);
        }
        /// <summary>
        /// Zaznamená do sebe referenci na stream a jeho pozici při počátku načítání dat ze streamu.
        /// Je voláno v LoadFrom() a LoadDirect().
        /// </summary>
        /// <param name="stream"></param>
        protected void StreamInit(ParserStream stream)
        {
            this.Stream = stream;
            this.StreamPointer = stream.Pointer;
            this.StreamLength = 0;
        }
        /// <summary>
        /// Zaznamená do sebe referenci na stream a jeho pozici při konci načítání dat ze streamu.
        /// Je voláno v LoadFrom() a LoadDirect().
        /// </summary>
        /// <param name="stream"></param>
        protected void StreamDone(ParserStream stream)
        {
            this.StreamLength = stream.Pointer - this.StreamPointer;
        }
        #endregion
    }
    #endregion
    #region interface ISqlCommand : definuje povinnosti pro třídy, které představují SQL příkaz
    /// <summary>
    /// Předpis pro všechny objekty, které představují SQL příkaz
    /// </summary>
    public interface ISqlCommand : IPlugin, ISqlPart
    {
        /// <summary>
        /// Název SQL příkazu
        /// </summary>
        string SqlCommand { get; }
    }
    #endregion
    #region interface ISqlPart : dvě metody, které z čehokoli udělají část SQL commandu: IsMyPart() a LoadPart()
    /// <summary>
    /// Předpis pro všechny objekty, které do sebe dokážou načíst určité části SQL příkazu
    /// </summary>
    public interface ISqlPart
    {
        /// <summary>
        /// Objekt zjistí, zda pro aktuální stream SQL položek je tento objekt ten pravý pro zpracování (detekuje úvodní klíčová slova).
        /// Stream se neposouvá, ale prvek sám může ze streamu číst (i přeskakovat komentáře) díky technice streamu using (stream.Preview()) { stream.Find...() }
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        bool IsMyPart(ParserStream stream);
        /// <summary>
        /// Metoda může být volaná kdykoliv, za jakéhokoli stavu streamu - tedy i když stream neobsahuje data pro tuto parts.
        /// Objekt pak nesmí stream posunout, ale musí v klidu skončit (ani chybu nemá vyhodit).
        /// Tato technika je potřebná pro sekvenční načítání vnitřních částí SQL příkazu, kdy SqlCommand obsahuje sekvenci příkazů Part.LoadForm() pro jednotlivé své SqlParts.
        /// Pořadí částí v SQL příkazu je povinné, proto je možno v pevně daném pořadí načítat jednotlivé Parts.
        /// Některé Parts jsou nepovinné, takže mají zůstat prázdné.
        /// Tato metoda v takovém případě uloží do objektu příznak "Not used part".
        /// Objekt do sebe načte data přicházející ze streamu, stream se tím posouvá.
        /// Jakmile objekt narazí na část streamu, kterou nezná, skončí.
        /// </summary>
        /// <param name="stream"></param>
        void LoadFrom(ParserStream stream);
    }
    #endregion
    #region Testovací třída
    public static class SqlCommandTest
    {
        public static void Run()
        {
            string sql = CreateCommand(2);
            var list = SqlCommands.SqlCommand.Parse(sql);
        }
        private static string CreateCommand(int command)
        {
            switch (command)
            {
                case 1: return Command1;
                case 2: return Command2;
                case 3: return Command3;
            }
            return "";
        }
        private static string Command1
        {
            get
            {
                return @"SELECT
p.cislo_subjektu,
p.cislo_objektu,
p.zdroj,
/* p.sklad, */
isnull(skl.sklad_mrp,skl.cislo_subjektu) sklad_mrp,
case st.povin_dim_varianta_zbozi when 'A' then p.varianta_zbozi else null end varianta_zbozi,
case st.povin_dim_obchodni_pripad when 'A' then p.obchodni_pripad else null end obchodni_pripad,
(select min(cislo_podkladu) from lcs. podklady pod where pod.cislo_subjektu = p.podklad) as cislo_podkladu,
max(pocet) / min(pocet) as pomer_maxmin,
p.typ,
p.pocet qty,
p.pocet_real qty_real,
p.datum,
/* case when (getdate() > p.datum) then getdate() else p.datum end datum, */
p.datum_plan,
/* case when (getdate() > p.datum_plan) then getdate() else p.datum_plan end datum_plan, */
/*  DAJ: následující 4 řádky jsou data pro doplňování stavu skladu, metodou stavSkladuCreateAndReFill() */
st.cislo_nonsubjektu as st_cislo_nonsubjektu,
p.varianta_zbozi as p_varianta_zbozi,
p.obchodni_pripad as p_obchodni_pripad,
'A' as refill_var_and_ord,
st.dodavka,
st.rezerva,
p.referencni_cislo reference_number,
h.sklad2 sklad2
FROM lcs.sk_polozka p
join lcs.sk_sklad skl on p.sklad=skl.cislo_subjektu
left outer join lcs.stav_sk st on st.zdroj=p.zdroj and st.sklad=isnull(skl.sklad_mrp,skl.cislo_subjektu)
join lcs.sk_hlavicka h on h.cislo_subjektu = p.cislo_subjektu
/*  DAJ: akceptovat pouze položky dokladu, kde je navázaná kmenová karta skladu, a ne obecný zdroj : */
join lcs.kmenova_karta_skladu kkzm on kkzm.cislo_subjektu = p.zdroj
WHERE p.stav = 1
/* and p.cislo_objektu = 5365 */
/*  and p.zdroj = 72467 */
and p.typ in ('3','4','5')
and (p.pocet - p.pocet_real) > 0
and p.splneno_uziv = 'N'
and skl.planovat = 'A' /* mrp sklad má plánovat vždy, zohledňuji normální sklad */";
            }
        }
        private static string Command2
        {
            get
            {
                return @"SELECT
pos.product_order cislo_subjektu,
null cislo_objektu,
pos.cislo_nonsubjektu cislo_nonsubjektu,
pos.source source,
isnull(skl.sklad_mrp,skl.cislo_subjektu) sklad_mrp,
pos.variation variation,
pos.order_relation order_relation,
'Q' typ,
pos.qty qty,
/*  DAJ: jako REALIZOVÁNO se pro MRP bere REALIZOVÁNO + REZERVOVÁNO (vývoj 0028095)   */
/*  pos.qty_real qty_real,  */
(isnull(pos.qty_real,0) + isnull(pos.qty_booked, 0)) as qty_real,
pos.date_plan date_plan,
pos.a_constr_element a_constr_element,
pos.constr_element constr_element,      /* DAJ 0029613 */
pos.plan_unit_s plan_unit_s,
pos.constr_element constr_element,
/*  DAJ: následující 1 řádek obsahuje data pro doplňování stavu skladu, metodou stavSkladuCreateAndReFill() */
st.cislo_nonsubjektu as st_cislo_nonsubjektu,
st.dodavka,
st.rezerva,
pos.status
FROM lcs.product_order_structure pos
join lcs.product_order po on po.cislo_subjektu = pos.product_order
join lcs.sk_sklad skl on pos.stock=skl.cislo_subjektu
left outer join lcs.stav_sk st on st.zdroj=pos.source and st.sklad=isnull(skl.sklad_mrp,skl.cislo_subjektu)
WHERE
skl.planovat = 'A'
/* DAJ 0026413 28.8.2007 : uvádět pouze komponenty na neuzavřených VP, s nenulovým zůstatkem, ve stavu pouze N */
and po.status IN ('N','P','J') and pos.status = 'N'
/* DAJ 0027542 21.1.2008 : množství qty_real ne zcela naplňuje požadované množství qty (obojí může být kladné i záporné) */
and (sign(pos.qty - pos.qty_real) = sign(pos.qty))
and pos.product_order in
(  /* mají nějakou nevydanou komponentu */
(select product_order
from lcs.product_order_structure pos1
where pos1.product_order = pos.product_order
and pos1.status not in ('A','J')
group by pos1.product_order
/* having sum(pos1.qty - pos1.qty_real) > 0)  */
having sum(case when (sign(pos1.qty - pos1.qty_real) = sign(pos1.qty)) then 1 else 0 end) > 0)
union
/* mají neodvedvený zůstatek na VP */
(select cislo_subjektu from lcs.product_order po1
where po1.qty_source - po1.qty_real - po1.qty_reject > 0
and po1.status IN ('N','P'))
)";
            }
        }
        private static string Command3
        {
            get
            {
                return @"
SELECT
 t1.cislo_subjektu as subjekt,
 max(t2.pocet) as maxpocet,
 (select q3.stav from table3 q3 where q3.klic = t1.klic) as q3stav
from 
 [database].[case].[where] t1
   left join tabulka2 as t2 with (readuncommitted) on t2.klic = t1.key,
 (select * from table4 q4) q4
   left outer hash join table6 t6 on t6.cislo= t1.key
where a=2 and q4.klic = t1.key
order by 1 desc
group by column1
";
            }
        }
    }
    #endregion
}
